package com.google.code.swingonfire.component.clock;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.Rectangle2D;
import java.util.Calendar;

import com.google.code.swingonfire.border.ShadowCircle;
import com.google.code.swingonfire.plotter.MoonSketch;
import com.google.code.swingonfire.plotter.Plotter;
import com.google.code.swingonfire.plotter.SunSketch;
import com.google.code.swingonfire.utility.Colors;
import com.google.code.swingonfire.utility.Utils;
import com.google.code.swingonfire.utility.date.DateFormatStyle;
import com.google.code.swingonfire.utility.date.DateRenderer;

public class DefaultJClockRenderer implements JClockRenderer
{

    public static class Tick
    {
        private final Color color;
        private final Stroke stoke;
        private final int length;

        public Tick(Color color, Stroke stoke, int length)
        {
            super();
            this.color = color;
            this.stoke = stoke;
            this.length = length;
        }

        public Color getColor()
        {
            return color;
        }

        public Stroke getStoke()
        {
            return stoke;
        }

        public int getLength()
        {
            return length;
        }

        protected void draw(Graphics2D g)
        {
            g.setColor(color);
            g.setStroke(stoke);
            g.drawLine(0, -256 + 16, 0, -256 + 16 + length);
        }
    }

    private final Plotter sunPlotter = new Plotter(new SunSketch(12));
    private final Plotter moonPlotter = new Plotter(new MoonSketch());

    private final ShadowCircle outerShadow = ShadowCircle.create();
    private final Color backgroundColor = Color.WHITE;
    private final Tick majorTicks = new Tick(new Color(0x80000000, true), new BasicStroke(8f, BasicStroke.CAP_SQUARE,
        BasicStroke.JOIN_ROUND), 48);
    private final Tick mediumTicks = new Tick(new Color(0x80000000, true), new BasicStroke(8f, BasicStroke.CAP_SQUARE,
        BasicStroke.JOIN_ROUND), 32);
    private final Tick minorTicks = new Tick(new Color(0x40000000, true), new BasicStroke(4f, BasicStroke.CAP_SQUARE,
        BasicStroke.JOIN_ROUND), 4);
    private Color borderColor;

    private final DateRenderer dateRenderer = DateRenderer.create();

    public DefaultJClockRenderer()
    {
        super();
    }

    @Override
    public void drawForeground(JClock clock, Graphics2D g, int width, int height)
    {
        int size = Math.min(width, height);
        int radius = calcRadiusWithoutShadow(size / 2);

        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);

        JClockState state = clock.getState();

        drawDateHand(clock, state.get(JClockHandType.YEAR), state.get(JClockHandType.MONTH),
            state.get(JClockHandType.DAY), (Graphics2D) g.create(), radius);
        drawTimeHand(clock, state.get(JClockHandType.HOUR), state.get(JClockHandType.MINUTE),
            state.get(JClockHandType.SECOND), (Graphics2D) g.create(), radius);
        drawSunHand(clock, state.get(JClockHandType.SUN), (Graphics2D) g.create(), radius);
        drawAMPMHand(clock, state.get(JClockHandType.AMPM), (Graphics2D) g.create(), radius);
        drawHourHand(clock, state.get(JClockHandType.HOUR), (Graphics2D) g.create(), radius);
        drawMinuteHand(clock, state.get(JClockHandType.MINUTE), (Graphics2D) g.create(), radius);
        drawSecondHand(clock, state.get(JClockHandType.SECOND), (Graphics2D) g.create(), radius);
    }

    @Override
    public double toAngle(JClock clock, JClockHandType type, double value)
    {
        switch (type)
        {
            case YEAR:
                throw new UnsupportedOperationException("toAngle for " + type + " not yet supported");
            case MONTH:
                throw new UnsupportedOperationException("toAngle for " + type + " not yet supported");
            case DAY:
                throw new UnsupportedOperationException("toAngle for " + type + " not yet supported");
            case SUN:
                return toSunHandAngle(value);
            case AMPM:
                throw new UnsupportedOperationException("toAngle for " + type + " not yet supported");
            case HOUR:
                return toHourHandAngle(value);
            case MINUTE:
                return toMinuteHandAngle(value);
            case SECOND:
                return toSecondHandAngle(value);
            default:
                throw new UnsupportedOperationException("toAngle for " + type + " not yet supported");
        }
    }

    @Override
    public double fromAngle(JClock clock, JClockHandType type, double value)
    {
        switch (type)
        {
            case YEAR:
                throw new UnsupportedOperationException("fromAngle for " + type + " not yet supported");
            case MONTH:
                throw new UnsupportedOperationException("fromAngle for " + type + " not yet supported");
            case DAY:
                throw new UnsupportedOperationException("fromAngle for " + type + " not yet supported");
            case SUN:
                return fromSunHandAngle(value);
            case AMPM:
                throw new UnsupportedOperationException("fromAngle for " + type + " not yet supported");
            case HOUR:
                return fromHourHandAngle(value);
            case MINUTE:
                return fromMinuteHandAngle(value);
            case SECOND:
                return fromSecondHandAngle(value);
            default:
                throw new UnsupportedOperationException("fromAngle for " + type + " not yet supported");
        }
    }

    @Override
    public JClockHandType getHit(JClock clock, int x, int y, int width, int height)
    {
        if (isHourHandHit(clock, x, y, width, height))
        {
            return JClockHandType.HOUR;
        }

        if (isMinuteHandHit(clock, x, y, width, height))
        {
            return JClockHandType.MINUTE;
        }

        if (isSecondHandHit(clock, x, y, width, height))
        {
            return JClockHandType.SECOND;
        }

        return null;
    }

    protected void drawDateHand(JClock clock, JClockHandState yearState, JClockHandState monthState,
        JClockHandState dayState, Graphics2D g, int radius)
    {
        
        g.scale(radius / 256d, radius / 256d);

        String text =
            String.format("%5.2f/%5.2f/%7.2f", dayState.getCurrent(), monthState.getCurrent(), yearState.getCurrent());

        Font font = clock.getFont().deriveFont(Font.BOLD, 24f);

        g.setFont(font);

        dateRenderer.setFont(clock.getFont().deriveFont(Font.BOLD, 24f), g.getFontRenderContext());
        g.setColor(Color.BLUE);
        dateRenderer.render(g, Calendar.getInstance());
        
        FontMetrics metrics = g.getFontMetrics();
        Rectangle2D bounds = metrics.getStringBounds(text, g);

        g.setColor(Color.ORANGE);
        g.drawString(text, (int) (-bounds.getWidth() / 2), 160);
    }

    protected void drawTimeHand(JClock clock, JClockHandState hourState, JClockHandState minuteState,
        JClockHandState secondState, Graphics2D g, int radius)
    {
        g.scale(radius / 256d, radius / 256d);

        String text =
            String.format("%5.2f:%5.2f:%5.2f", hourState.getCurrent(), minuteState.getCurrent(),
                secondState.getCurrent());

        Font font = clock.getFont().deriveFont(Font.BOLD, 24f);

        g.setFont(font);

        FontMetrics metrics = g.getFontMetrics();
        Rectangle2D bounds = metrics.getStringBounds(text, g);

        g.setColor(Color.ORANGE);
        g.drawString(text, (int) (-bounds.getWidth() / 2), 80);
    }

    protected void drawSunHand(JClock clock, JClockHandState state, Graphics2D g, int radius)
    {
        g.scale(radius / 256d, radius / 256d);

        double theta = toSunHandAngle(state.getCurrent());
        double x = -Math.sin(theta);
        double y = Math.cos(theta);

        g.setColor(Colors.translucent(Color.LIGHT_GRAY, 0.5 - (Utils.bound(-0.25, y, 0.25) * 2)));

        sunPlotter.size(64).draw(g, (int) (x * 128), (int) (y * 128));

        g.setColor(Colors.translucent(Color.LIGHT_GRAY, 0.5 + (Utils.bound(-0.25, y, 0.25) * 2)));

        moonPlotter.size(64).draw(g, (int) (-x * 128), (int) (-y * 128));
    }

    public double toSunHandAngle(double sun)
    {
        return Math.PI * 2 * sun;
    }

    public double fromSunHandAngle(double theta)
    {
        return theta / (Math.PI * 2);
    }

    protected void drawAMPMHand(JClock clock, JClockHandState state, Graphics2D g, int radius)
    {
        g.scale(radius / 256d, radius / 256d);

        double current = state.getCurrent();

        while (current > 2)
        {
            current -= 2;
        }

        while (current < 0)
        {
            current += 2;
        }

        String text = (current < 1) ? "AM" : "PM";

        g.setColor(Colors.translucent(Color.LIGHT_GRAY, 1 - (Math.cos(current * Math.PI * 2))));

        Font font = clock.getFont().deriveFont(Font.BOLD, 48f);

        g.setFont(font);

        FontMetrics metrics = g.getFontMetrics();
        Rectangle2D bounds = metrics.getStringBounds(text, g);

        g.drawString(text, (int) (-bounds.getWidth() / 2), 128);
    }

    protected void drawHourHand(JClock clock, JClockHandState state, Graphics2D g, int radius)
    {
        g.scale(radius / 256d, radius / 256d);
        g.rotate(toHourHandAngle(state.getCurrent()));

        drawHourHandBackground(g);
        drawHourHandBorder(g);
        drawHourHandForeground(clock, state, g);
    }

    private void drawHourHandBackground(Graphics2D g)
    {
        g.setColor(Colors.translucent(Color.WHITE, 0.5));
        g.setStroke(new BasicStroke(18f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));

        drawHourHand(g);
    }

    private void drawHourHandBorder(Graphics2D g)
    {
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(16f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));

        drawHourHand(g);
    }

    private void drawHourHandForeground(JClock clock, JClockHandState state, Graphics2D g)
    {
        g.setStroke(new BasicStroke(14f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));
        g.setColor(getHourHandForeground(clock, state));

        drawHourHand(g);
    }

    private void drawHourHand(Graphics2D g)
    {
        g.drawLine(0, 0, 0, -getHourHandGrabLength());
        g.drawOval(-8, -8, 16, 16);
    }

    protected Color getHourHandForeground(JClock clock, JClockHandState state)
    {
        return (state.isSelected()) ? new Color(0x3399ff) : Color.BLACK;
    }

    protected int getHourHandGrabLength()
    {
        return 164;
    }

    protected int getHourHandGrabWidth()
    {
        return 32;
    }

    public double toHourHandAngle(double hour)
    {
        return (Math.PI / 6) * hour;
    }

    public double fromHourHandAngle(double theta)
    {
        return theta / (Math.PI / 6);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isHourHandHit(JClock clock, int x, int y, int width, int height)
    {
        double theta = toHourHandAngle(clock.getState().get(JClockHandType.HOUR).getCurrent());
        int grabLength = getHourHandGrabLength();
        int grabWidth = getHourHandGrabWidth();

        return isHandHit(x, y, width, height, theta, grabLength, grabWidth);
    }

    protected void drawMinuteHand(JClock clock, JClockHandState state, Graphics2D g, int radius)
    {
        g.scale(radius / 256d, radius / 256d);
        g.rotate(toMinuteHandAngle(state.getCurrent()));

        drawMinuteHandBackground(g);
        drawMinuteHandBorder(g);
        drawMinuteHandForeground(clock, state, g);
    }

    private void drawMinuteHandBackground(Graphics2D g)
    {
        g.setColor(Colors.translucent(Color.WHITE, 0.5));
        g.setStroke(new BasicStroke(14f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));

        drawMinuteHand(g);
    }

    private void drawMinuteHandBorder(Graphics2D g)
    {
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(12f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));

        drawMinuteHand(g);
    }

    private void drawMinuteHandForeground(JClock clock, JClockHandState state, Graphics2D g)
    {
        g.setStroke(new BasicStroke(10f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));
        g.setColor(getMinuteHandForeground(clock, state));

        drawMinuteHand(g);
    }

    private void drawMinuteHand(Graphics2D g)
    {
        g.drawLine(0, 0, 0, -getMinuteHandGrabLength());
        g.drawOval(-8, -8, 16, 16);
    }

    protected Color getMinuteHandForeground(JClock clock, JClockHandState state)
    {
        return (state.isSelected()) ? new Color(0x3399ff) : Color.BLACK;
    }

    protected int getMinuteHandGrabLength()
    {
        return 224;
    }

    protected int getMinuteHandGrabWidth()
    {
        return 24;
    }

    protected double toMinuteHandAngle(double second)
    {
        return (Math.PI / 30) * second;
    }

    protected double fromMinuteHandAngle(double theta)
    {
        return theta / (Math.PI / 30);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isMinuteHandHit(JClock clock, int x, int y, int width, int height)
    {
        double theta = toMinuteHandAngle(clock.getState().get(JClockHandType.MINUTE).getCurrent());
        int grabLength = getMinuteHandGrabLength();
        int grabWidth = getMinuteHandGrabWidth();

        return isHandHit(x, y, width, height, theta, grabLength, grabWidth);
    }

    protected void drawSecondHand(JClock clock, JClockHandState state, Graphics2D g, int radius)
    {
        g.scale(radius / 256d, radius / 256d);
        g.rotate(toSecondHandAngle(state.getCurrent()));

        drawSecondHandBackground(g);
        drawSecondHandBorder(g);
        drawSecondHandForeground(clock, state, g);
    }

    private void drawSecondHandBackground(Graphics2D g)
    {
        g.setColor(Colors.translucent(Color.WHITE, 0.5));
        g.setStroke(new BasicStroke(6f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));

        drawSecondHand(g);

        g.setStroke(new BasicStroke(18f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));

        drawSecondHandExtend(g);
    }

    private void drawSecondHandBorder(Graphics2D g)
    {
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(4f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));

        drawSecondHand(g);

        g.setStroke(new BasicStroke(16f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));

        drawSecondHandExtend(g);
    }

    private void drawSecondHandForeground(JClock clock, JClockHandState state, Graphics2D g)
    {
        g.setStroke(new BasicStroke(2f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));
        g.setColor(getSecondHandForeground(clock, state));

        drawSecondHand(g);

        g.setStroke(new BasicStroke(14f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));

        drawSecondHandExtend(g);
    }

    private void drawSecondHand(Graphics2D g)
    {
        g.drawLine(0, 0, 0, -getSecondHandGrabLength());
        g.drawOval(-8, -8, 16, 16);
    }

    private void drawSecondHandExtend(Graphics2D g)
    {
        g.drawLine(0, 64, 0, 0);
    }

    private Color getSecondHandForeground(JClock clock, JClockHandState state)
    {
        return (state.isSelected()) ? new Color(0x3399ff) : new Color(0x7f1919);
    }

    protected int getSecondHandGrabLength()
    {
        return 224;
    }

    protected int getSecondHandGrabWidth()
    {
        return 8;
    }

    protected double toSecondHandAngle(double second)
    {
        return (Math.PI / 30) * second;
    }

    protected double fromSecondHandAngle(double theta)
    {
        return theta / (Math.PI / 30);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isSecondHandHit(JClock clock, int x, int y, int width, int height)
    {
        double theta = toSecondHandAngle(clock.getState().get(JClockHandType.SECOND).getCurrent());
        int grabLength = getSecondHandGrabLength();
        int grabWidth = getSecondHandGrabWidth();

        return isHandHit(x, y, width, height, theta, grabLength, grabWidth);
    }

    private boolean isHandHit(int x, int y, int width, int height, double theta, int grabLength, int grabWidth)
    {
        int size = Math.min(width, height);
        int radius = calcRadiusWithoutShadow(size / 2);
        double a = Math.sin(theta);
        double b = -Math.cos(theta);
        double c = (a * a) + (b * b);
        double d = (c != 0) ? ((a * x) + (b * y)) / c : 0;
        double e = (x - (a * d)) / b;
        double maxD = (grabLength / 256d) * radius;

        if (d < 0)
        {
            return false;
        }
        else if (d > maxD)
        {
            return false;
        }
        else if (Math.abs(e) > (grabWidth / 2))
        {
            return false;
        }

        return true;
    }

    @Override
    public void drawBackground(JClock clock, Graphics2D g, int size)
    {
        int radius = calcRadiusWithoutShadow(size / 2);

        if (outerShadow != null)
        {
            outerShadow.paint(g, 0, 0, radius);
        }

        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);

        g.setColor(backgroundColor);
        g.fillOval(-radius, -radius, radius * 2, radius * 2);

        drawTicks((Graphics2D) g.create(), radius);
    }

    private int calcRadiusWithoutShadow(int radius)
    {
        if (outerShadow != null)
        {
            radius -=
                Math.max(Math.max(Math.abs(outerShadow.getInsets().top), Math.abs(outerShadow.getInsets().bottom)),
                    Math.max(Math.abs(outerShadow.getInsets().left), Math.abs(outerShadow.getInsets().right)));
        }

        return radius;
    }

    protected void drawTicks(Graphics2D g, int radius)
    {
        g.scale(radius / 256d, radius / 256d);
        drawMajorTicks(g);
    }

    protected void drawMajorTicks(Graphics2D g)
    {
        if (majorTicks != null)
        {
            for (int i = 0; i < 4; i += 1)
            {
                drawMajorTick((Graphics2D) g.create(), i);
            }
        }
    }

    protected void drawMajorTick(Graphics2D g, int index)
    {
        g.rotate((Math.PI / 2) * index);
        majorTicks.draw(g);
        drawMinorTicks(g);
        drawMediumTicks(g);
    }

    protected void drawMediumTicks(Graphics2D g)
    {
        if (mediumTicks != null)
        {
            for (int i = 1; i < 3; i += 1)
            {
                drawMediumTick((Graphics2D) g.create(), i);
            }
        }
    }

    protected void drawMediumTick(Graphics2D g, int index)
    {
        g.rotate((Math.PI / 6) * index);
        mediumTicks.draw(g);
        drawMinorTicks(g);
    }

    protected void drawMinorTicks(Graphics2D g)
    {
        if (minorTicks != null)
        {
            for (int i = 1; i < 5; i += 1)
            {
                drawMinorTick((Graphics2D) g.create(), i);
            }
        }
    }

    protected void drawMinorTick(Graphics2D g, int index)
    {
        g.rotate((Math.PI / 30) * index);
        minorTicks.draw(g);
    }

}
